﻿/* Copyright 2010 Igor Francé
 * 
 * This file is part of ECMADoc (http://ecmadoc.codeplex.com).
 * 
 * ECMADoc is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * ECMADoc is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with ECMADoc. 
 * If not, see http://www.gnu.org/licenses/.
 */

namespace EcmaDoc.Core
{
	using System;
	using System.Collections.Generic;
	using System.IO;
	using System.Linq;
	using System.Reflection;
	using System.Text.RegularExpressions;
	using System.Xml;

	using EcmaDoc.Core.Configuration;
	using EcmaDoc.Core.Items;
	using EcmaDoc.Core.Output;
	using EcmaDoc.Core.Resources;

	using log4net;

	/// <summary>
	/// Implements a class that coordinates parsing of source files and generating output files for a whole project.
	/// </summary>
	/// <remarks>
	/// The complete processing of a project consists of the following steps:
	/// <list type="number">
	/// <item><description>Discover all files that are a part of this project.</description></item>
	/// <item><description>Parse each file and generate xml document artifact for it.</description></item>
	/// <item><description>Combine the information from all files into project xml artifacts.</description></item>
	/// <item><description>(Resolve types, resolve links, assign full names).</description></item>
	/// <item><description>Build project index.</description></item>
	/// <item><description>Build project contents.</description></item>
	/// <item><description>Build a page for each project item.</description></item>
	/// </list>
	/// </remarks>
	public class Project
	{
		private static readonly Regex typeExpr1 = new Regex(@"<see type=('|"")(?'type'.*?)\1\s*/>", RegexOptions.Compiled);

		private static readonly Regex typeExpr2 = new Regex(
			@"<see type=('|"")(?'type'.*?)\1\s*>(?'text'.*?)</see>", RegexOptions.Compiled);

		private static readonly Regex typeExpr3 = new Regex(@"\{@see\s+(?'type'\S+)\}", RegexOptions.Compiled);

		private static readonly Regex typeExpr4 = new Regex(
			@"(?'prefix'<c>)(?'type'\S+)(?'suffix'</c>)", RegexOptions.Compiled);

		private static readonly Regex markupExpr1 = new Regex(@"<c>(?'type'.*?)</c>", RegexOptions.Compiled);

		private static readonly Dictionary<string, MethodInfo> operations;

		private static readonly ILog log = LogManager.GetLogger(typeof(Project).FullName);

		private ProjectOperationProgressEventArgs progressArgs;

		/// <summary>
		/// Initializes static members of the <see cref="Project"/> class.
		/// </summary>
		static Project()
		{
			operations = new Dictionary<string, MethodInfo>();
			const BindingFlags bindings = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			foreach (MethodInfo method in typeof(Project).GetMethods(bindings))
			{
				object[] attribs = method.GetCustomAttributes(typeof(ProjectOperationAttribute), true);
				if (attribs.Length != 0)
				{
					var attrib = (ProjectOperationAttribute)attribs[0];
					operations.Add(attrib.Name, method);
				}
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="Project"/> class, using the specified project info instance.
		/// </summary>
		/// <param name="info">The project configuration to use to initialize this project.</param>
		internal Project(ProjectInfo info)
		{
			if (info.Config == null)
			{
				throw new ArgumentException("The project configuration cannot be null.", "info");
			}

			this.Info = info;
			this.Config = info.Config;
			this.Reset();
		}

		/// <summary>
		/// Gets a value indicating whether this <see cref="Project"/> is currently being processed.
		/// </summary>
		/// <value><c>true</c> if this <see cref="Project"/> is currently being processed; otherwise, <c>false</c>.</value>
		public bool Processing { get; private set; }

		/// <summary>
		/// Gets the <see cref="ProjectItem"/> equivalent of this project.
		/// </summary>
		public ProjectItem Item { get; private set; }

		/// <summary>
		/// Gets the list of files this project consists of.
		/// </summary>
		public List<string> Files { get; private set; }

		/// <summary>
		/// Gets the file path to the project member file generated at the end of the documentation generation process.
		/// </summary>
		public string ProjectFilePath
		{
			get
			{
				return this.Info.ExpandOutputPath("project.xml");
			}
		}

		/// <summary>
		/// Gets the file path to the project index file generated at the end of the documentation generation process.
		/// </summary>
		public string ProjectIndexPath
		{
			get
			{
				return this.Info.ExpandOutputPath("index.xml");
			}
		}

		/// <summary>
		/// Gets the parser configuration in use by this project.
		/// </summary>
		public ParserConfig Config { get; private set; }

		/// <summary>
		/// Gets the project configuration for this project.
		/// </summary>
		public ProjectInfo Info { get; private set; }

		/// <summary>
		/// Fired continuously as the parser progresses through parsing the code.
		/// </summary>
		public event Action<object, ProjectOperationProgressEventArgs> ProcessProgress;

		/// <summary>
		/// Creates an empty project instance.
		/// </summary>
		/// <returns>
		/// A default, empty project.
		/// </returns>
		public static Project Create()
		{
			return Create(new ProjectInfo());
		}

		/// <summary>
		/// Creates a project using the specified project path.
		/// </summary>
		/// <param name="projectPath">The path to the project definition file.</param>
		/// <returns>A project initialized using the definition path specified with <paramref name="projectPath"/>.</returns>
		public static Project Create(string projectPath)
		{
			var info = new ProjectInfo(projectPath);
			return Create(info);
		}

		/// <summary>
		/// Creates a project using the specified project info.
		/// </summary>
		/// <param name="info">The project configuration instance.</param>
		/// <returns>A project initialized using the specified project <paramref name="info"/>.</returns>
		public static Project Create(ProjectInfo info)
		{
			return new Project(info);
		}

		/// <summary>
		/// Executes the project operation with the specified <paramref name="name"/>.
		/// </summary>
		/// <param name="name">The name of the operation to execute.</param>
		public void ExecuteOperation(string name)
		{
			if (!operations.ContainsKey(name))
			{
				throw new ArgumentException(string.Format("Current project doesn't contain an operation with name '{0}'", name));
			}

			MethodInfo method = operations[name];
			method.Invoke(this, new object[] { });
		}

		[ProjectOperation("Process project")]
		internal void ProcessFull()
		{
			this.Reset();

			this.ProcessOperation(
				new ProjectOperation(
					"Process Project",
					new Dictionary<string, Action>
						{
							{ Strings.PreparingDestination, this.PrepareDestination },
							{ Strings.DiscoveringFiles, this.DiscoverFiles },
							{ Strings.ParsingFiles, this.ParseFiles },
							{ Strings.MergingResults, this.MergeResults },
							{ Strings.ResolvingTypes, this.ResolveTypes },
							{ Strings.ProcessingInheritance, this.ProcessInheritance },
							{ Strings.ProcessingInheritedMembers, this.ProcessInheritedMembers },
							{ Strings.GeneratingOutput, this.GenerateHtml },
						}));

			this.Save();
		}

		[ProjectOperation("Process object model")]
		internal void ProcessObjectModell()
		{
			this.Reset();

			this.ProcessOperation(
				new ProjectOperation(
					"Process Object Model",
					new Dictionary<string, Action>
						{
							{ Strings.DiscoveringFiles, this.DiscoverFiles },
							{ Strings.ParsingFiles, this.ParseFiles },
							{ Strings.MergingResults, this.MergeResults },
							{ Strings.ResolvingTypes, this.ResolveTypes },
							{ Strings.ProcessingInheritance, this.ProcessInheritance },
							{ Strings.ProcessingInheritedMembers, this.ProcessInheritedMembers },
						}));

			this.Save();
		}

		[ProjectOperation("Regenerate output")]
		internal void RegenerateOutput()
		{
			this.ProcessOperation(
				new ProjectOperation(
					"Regenerate Output", new Dictionary<string, Action> { { Strings.GeneratingOutput, this.ReGenerateHtml }, }));
		}

		[ProjectOperation("Regenerate index")]
		internal void RegenerateIndex()
		{
			this.ProcessOperation(
				new ProjectOperation(
					"Regenerate Index",
					new Dictionary<string, Action> { { Strings.GeneratingOutput, () => this.GenerateHtml(HtmlPageType.Index) }, }));
		}

		[ProjectOperation("Regenerate contents")]
		internal void RegenerateContents()
		{
			this.ProcessOperation(
				new ProjectOperation(
					"Regenerate Contents",
					new Dictionary<string, Action> { { Strings.GeneratingOutput, () => this.GenerateHtml(HtmlPageType.Contents) }, }));
		}

		/// <summary>
		/// Ensures that the target directory exists and is empty.
		/// </summary>
		[OperationStep]
		internal void PrepareDestination()
		{
			if (Directory.Exists(this.Info.OutputPathFull))
			{
				Log.WriteLine(LogCategory.Debug, "Deleting directory '{0}'.", this.Info.OutputPathFull);
				Directory.Delete(this.Info.OutputPathFull, true);
			}

			Log.WriteLine(LogCategory.Debug, "Creating directory '{0}'.", this.Info.OutputPathFull);
			Directory.CreateDirectory(this.Info.OutputPathFull);
		}

		/// <summary>
		/// Discovers all files that are a part of this project.
		/// </summary>
		[OperationStep]
		internal void DiscoverFiles()
		{
			if (this.Files == null || this.Files.Count == 0)
			{
				this.Files = new List<string>();

				int sourceCount = this.Info.Files.Count + this.Info.Locations.Count;
				int currentIndex = 0;
				foreach (var file in this.Info.Files)
				{
					string path = this.Info.ResolvePath(file);
					Log.WriteLine(
						LogCategory.Debug,
						"Adding statically defined file '{0}' (resolved: {1}) to the list of files to process.",
						file,
						path);

					this.Files.Add(path);
					FireProgressEvent(file, 100, this.GetPercentDone(sourceCount, ++currentIndex));
				}

				foreach (var pathInfo in this.Info.Locations)
				{
					string path = this.Info.ResolvePath(pathInfo.Path);
					Log.WriteLine(LogCategory.Debug, "Scanning path '{0}' (resolved: {1}) for files to process.", pathInfo.Path, path);

					string[] files = this.GetFiles(pathInfo).ToArray();

					Log.WriteLine(
						LogCategory.Debug,
						"Adding dynamically discovered files for folder '{0}' (extensions: {1}) to the list of files to process: {2}",
						pathInfo.Path,
						pathInfo.Extensions,
						files.Join(", "));
					this.Files.AddRange(files);

					FireProgressEvent(pathInfo.Path, 100, this.GetPercentDone(sourceCount, ++currentIndex));
				}
			}
		}

		/// <summary>
		/// Parses all files that are a part of this project.
		/// </summary>
		[OperationStep]
		internal void ParseFiles()
		{
			try
			{
				int fileIndex = 0;

				ParserProgressEventHandler onParserEvent =
					(sender, args) =>
					FireProgressEvent(args.FileName, args.PercentDone, this.GetPercentDone(this.Files.Count, fileIndex));

				var parser = new Parser(this.Config);
				parser.Progress += onParserEvent;

				foreach (string fileName in this.Files)
				{
					fileIndex++;

					Log.WriteLine(LogCategory.Debug, "Parsing file '{0}'...", fileName);

					DocumentItem fileItem = this.ParseFile(this.Item, parser, fileName);
					this.Item.AddDocument(fileItem);
				}
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error parsing files: {0}", ex.Message);
				throw;
			}
		}

		/// <summary>
		/// Merges the parsed artifacts within their project item.
		/// </summary>
		/// <remarks>
		/// Merging results is a process of unifying the separate parsed artifacts into a complete project structure.
		/// It consists of following steps:
		/// <list type="table">
		/// <listheader>
		/// <term>Step</term>
		/// <description>Comment</description>
		/// </listheader>
		/// <item>
		/// <term>Add namespaces</term>
		/// <description>Adds all namespaces found across documents to the main project item.</description>
		/// </item>
		/// <item>
		/// <term>Move non-global members to corresponding namespaces within the main project item</term>
		/// <description>Moves all non-global members from project documents to their corresponding namespaces
		/// (using their <see cref="CodeItem.NamePrefix"/>) within the main project item. Subsequently, loop 
		/// through the hierarchy of previous parents of the member that got moved and remove them if they have 
		/// no other members left within them.</description>
		/// </item>
		/// <item>
		/// <term>Move global members to the global namespace within the main project item</term>
		/// <description>Moves all remaining global members from project document to the main project item.</description>
		/// </item>
		/// <item>
		/// <term>Process statics</term>
		/// <description>This step loops recursively through all static classes and enum, regardless of their depth
		/// and moves them to their owner's parent. It then prepends to their names the names of their owner.</description>
		/// </item>
		/// </list>
		/// </remarks>
		[OperationStep]
		internal void MergeResults()
		{
			try
			{
				var subject1 = (ProjectItem)this.Item.ShallowCopy();

				// create namespaces found in all project documents in the project item
				foreach (DocumentItem doc in subject1.Documents)
				{
					foreach (NamespaceItem ns in doc.Namespaces.Values)
					{
						Log.WriteLine(LogCategory.Debug, "Adding namespace {0}, found in document {1}.", ns.Name, doc.Name);
						subject1.AddNamespace(ns.ExpandedName, ns.Comment);
					}
				}

				// create a collection with ALL parsed members
				var parsedMembers = new List<CodeItem>();
				subject1.Documents.ForEach(d => parsedMembers.AddRange(d.MembersRecursive));

				int stepCount = parsedMembers.Count();
				int currentStep = 0;

				Log.WriteLine(LogCategory.Debug, "Total parsed member count is {0}.", stepCount);

				// move each parsed member to its corresponding namespace
				foreach (CodeItem ns in subject1.Namespaces.Values.OrderByDescending(ns => ns.Name))
				{
					if (ns.Name == ParserConfig.GlobalNamespace)
					{
						continue;
					}

					var items = parsedMembers.Where(m => (m.NamePrefix == ns.Name));
					foreach (CodeItem item in items)
					{
						FireProgressEvent(item.Name, 0, this.GetPercentDone(stepCount, ++currentStep));

						if (!(item.IsVirtual && item.Members.Count == 0))
						{
							Log.WriteLine(LogCategory.Debug, "Adding member {0} to namespace {1}.", item.Name, ns.Name);
							CodeItem parent = item.Parent;
							Visibility visibility = item.Visibility == Visibility.Private ? Visibility.Private : Visibility.Public;

							ns.AddMember(item.Name, visibility, item);
							parent.RemoveMember(item.Name);

							while (parent != null && parent.MemberCount == 0 && !(parent is NamespaceItem))
							{
								parent.Parent.RemoveMember(parent.Name);
								parent = parent.Parent;
							}
						}

						this.FireProgressEvent(100);
					}
				}

				// move remaining members to the global namespace
				foreach (DocumentItem doc in subject1.Documents)
				{
					foreach (CodeItem item in doc.Members)
					{
						FireProgressEvent(item.Name, 0, this.GetPercentDone(stepCount, ++currentStep));
						if (!(item.IsVirtual && item.Members.Count == 0))
						{
							Log.WriteLine(LogCategory.Debug, "Adding member {0} to global namespace.", item.Name);

							subject1.GlobalNamespace.AddMember(item.Name, item.Visibility, item);
						}

						this.FireProgressEvent(100);
					}
				}

				// move all static classes and enumerations to their owner's parent
				foreach (CodeItem ns in subject1.Namespaces.Values)
				{
					foreach (
						CodeItem member in
							ns.MembersRecursive.Where(m => m.Visibility == Visibility.Static && !(m.Parent is NamespaceItem)))
					{
						bool isClass = member.ItemType == ItemType.Class;
						bool isEnum = member.ItemType == ItemType.Enumeration;

						if (isClass || isEnum)
						{
							CodeItem owner = member.Parent;
							CodeItem parent = owner.Parent;

							owner.MemberGroups[Visibility.Static].Remove(member.Name);
							member.Name = member.Parent.Name + "." + member.Name;

							Log.WriteLine(LogCategory.Debug, "Adding static member {0} to {1}.", member.Name, parent.Name);
							parent.AddMember(member.Name, Visibility.Public, member);
						}
					}
				}

				// remove all empty namespaces

				var emptyNamespaces = new List<string>();
				foreach (
					CodeItem ns in
						subject1.Namespaces.Values.Where(ns => ns.Name != ParserConfig.GlobalNamespace && ns.MemberCount == 0))
				{
					Console.WriteLine("Adding empty: " + ns.Name);
					emptyNamespaces.Add(ns.Name);
				}

				foreach (string ns in emptyNamespaces)
				{
					Console.WriteLine("Removing empty: " + ns);
					subject1.Namespaces.Remove(ns);
				}
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error merging results: {0}", ex.Message);
				throw;
			}
		}

		/// <summary>
		/// Resolves any referenced types (@type, @returns, c tags etc).
		/// </summary>
		/// <remarks>
		/// process project, recursively:
		///   process item:
		///     all members: typeUrl
		///     all arguments: typeUrl
		///     comment: replace {@see|@link type} in description and tokens with typeUrl
		///     process item members
		///   ^-----
		/// </remarks>
		[OperationStep]
		internal void ResolveTypes()
		{
			try
			{
				this.Item.MembersRecursive.ForEach(i => i.UpdateMemberVisibilities());

				int stepCount = this.Item.MemberCountRecursive;
				int currentStep = 0;

				Action<CodeItem> processMember = null;
				processMember = delegate(CodeItem currentMember)
					{
						Log.WriteLine(LogCategory.Debug, "Processing types related to {0}", currentMember.FullName);

						FireProgressEvent(currentMember.Name, 0, this.GetPercentDone(stepCount, ++currentStep));

						// resolve the type of the current object
						if (!string.IsNullOrEmpty(currentMember.Type))
						{
							currentMember.TypeUrl = this.ResolveTypeUrl(currentMember.Type, currentMember);
						}

						// resolve functions
						if (currentMember is FunctionItem)
						{
							var currentFunction = (FunctionItem)currentMember;
							var arguments = new List<Argument>();
							arguments.AddRange(currentFunction.Arguments.Values);
							arguments.AddRange(currentFunction.DocumentedArguments.Values);

							// resolve the types of function arguments
							foreach (Argument arg in arguments)
							{
								arg.TypeUrl = this.ResolveTypeUrl(arg.Type, currentMember);

								if (!string.IsNullOrEmpty(arg.Description))
								{
									arg.Description = this.ProcessString(arg.Description, currentMember);
								}
							}

							// resolve the type this function returns, and process the return description
							if (currentFunction.Returns != null)
							{
								ReturnItem r = currentFunction.Returns;
								r.TypeUrl = this.ResolveTypeUrl(r.Type, currentMember);

								if (!string.IsNullOrEmpty(r.Description))
								{
									r.Description = this.ProcessString(r.Description, currentMember);
								}
							}

							if (!string.IsNullOrEmpty(currentFunction.VariableArgumentsDescription))
							{
								Log.WriteLine(LogCategory.Debug, "Processing variable arguments description text...");
								currentFunction.VariableArgumentsDescription = this.ProcessString(
									currentFunction.VariableArgumentsDescription, currentMember);
							}

							if (!string.IsNullOrEmpty(currentFunction.VariableArgumentsType))
							{
								currentFunction.VariableArgumentsTypeUrl = this.ResolveTypeUrl(
									currentFunction.VariableArgumentsType, currentMember);
							}
						}

						// process the contents of comment strings
						if (currentMember.Comment != null)
						{
							Comment c = currentMember.Comment;
							if (!string.IsNullOrEmpty(c.Summary))
							{
								c.Summary = this.ProcessString(c.Summary, currentMember);
							}

							if (!string.IsNullOrEmpty(c.Description))
							{
								c.Description = this.ProcessString(c.Description, currentMember);
							}
						}

						this.FireProgressEvent(100);

						foreach (CodeItem ctx in currentMember.Members)
						{
							processMember(ctx);
						}
					};

				processMember(this.Item);

				this.Save();
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error resolving types: {0}", ex.Message);
				throw;
			}
		}

		/// <summary>
		/// Processes the inheritance chain of all parsed classes.
		/// </summary>
		[OperationStep]
		internal void ProcessInheritance()
		{
			try
			{
				IEnumerable<CodeItem> classes =
					this.Item.MembersRecursive.Where(
						m => (m.ItemType == ItemType.Class) || (m.ItemType == ItemType.Object && m.Members.Count != 0));

				int stepCount = classes.Count();
				int currentStep = 0;

				Action<CodeItem, List<List<object>>> processMember = null;
				processMember = delegate(CodeItem type, List<List<object>> threads)
					{
						List<object> thread = threads[threads.Count - 1];
						List<object> currentList;
						CodeItem currentType = type;

						const ItemType allowedBaseTypes = ItemType.Object | ItemType.Function | ItemType.Class | ItemType.Enumeration;

						while (currentType != null && currentType.BaseTypes.Count != 0)
						{
							for (int i = currentType.BaseTypes.Count - 1; i >= 0; i--)
							{
								string baseName = currentType.BaseTypes[i];
								CodeItem baseType = this.ResolveType(baseName, currentType, allowedBaseTypes);

								if (baseType == currentType)
								{
									currentType = null;
									break;
								}

								// multiple inheritance
								if (i > 0)
								{
									currentList = thread.Duplicate<object>();
									threads.Add(currentList);
								}
								else
								{
									currentList = thread;
								}

								currentList.Add(baseType != null ? (object)baseType : baseName);

								// multiple inheritance
								if (i > 0)
								{
									processMember(baseType, threads);
								}
								else
								{
									currentType = baseType;
								}
							}
						}
					};

				foreach (CodeItem type in classes)
				{
					FireProgressEvent(type.Name, 0, this.GetPercentDone(stepCount, ++currentStep));

					Log.WriteLine(LogCategory.Debug, "Processing inheritance for {0}", type.FullName);

					type.Ancestors = new List<List<object>>();

					if (type.BaseTypes.Count != 0)
					{
						Log.WriteLine(
							LogCategory.Debug, "Type {0} has {1} custom base types defined.", type.FullName, type.BaseTypes.Count);

						var threads = new List<List<object>>();
						for (int i = 0; i < type.BaseTypes.Count; i++)
						{
							threads.Add(new List<object>());
							processMember(type, threads);
						}

						int maxCount = threads.Max(t => t.Count);
						for (int i = 0; i < maxCount; i++)
						{
							var curr = new List<object>();
							foreach (List<object> thread in threads)
							{
								if (thread.Count <= i || curr.Contains(thread[i]) || type.Ancestors.ContainsItem(thread[i]))
								{
									continue;
								}

								// for the first level (multiple) inheritance, immediate parents get
								// priority in being added
								if (i == 0 || !threads.ContainsItemAfterIndex(thread[i], i))
								{
									curr.Add(thread[i]);
								}
							}

							if (curr.Count != 0)
							{
								type.Ancestors.Add(curr);
							}
						}

						Log.WriteLine(
							LogCategory.Debug, "Type {0} has {1} ancestor types.", type.FullName, type.Ancestors.Sum(a => a.Count));
					}

					Log.WriteLine(LogCategory.Debug, "Type {0} doesn't have custom base types defined.", type.FullName);
					this.FireProgressEvent(100);
				}
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error processing inheritance: {0}", ex.Message);
				throw;
			}
		}

		[OperationStep]
		internal void ProcessInheritedMembers()
		{
			try
			{
				IEnumerable<CodeItem> classes =
					this.Item.MembersRecursive.Where(
						m => (m.ItemType == ItemType.Class) || (m.ItemType == ItemType.Object && m.Members.Count != 0));

				int stepCount = classes.Count();
				int currentStep = 0;

				foreach (CodeItem type in classes)
				{
					FireProgressEvent(type.Name, 0, this.GetPercentDone(stepCount, ++currentStep));

					if (type is FunctionItem && ((FunctionItem)type).IsStatic)
					{
						continue;
					}

					List<CodeItem> allAncestors = type.Ancestors.Flatten<CodeItem>();
					foreach (CodeItem parentType in allAncestors)
					{
						var parentMembers = new List<CodeItem>();
						parentMembers.AddRange(parentType.MemberGroups[Visibility.Prototype].Values);
						parentMembers.AddRange(parentType.MemberGroups[Visibility.Instance].Values);

						foreach (CodeItem parentMember in parentMembers)
						{
							CodeItem childMember = type.GetMember(parentMember.Name, Visibility.Instance) ??
							                       type.GetMember(parentMember.Name, Visibility.Prototype);

							if (childMember != null)
							{
								childMember.Overrides = parentMember;
							}
							else
							{
								if (type.InheritedMembers.ContainsKey(parentMember.Name))
								{
									type.InheritedMembers[parentMember.Name].Overrides = parentMember;
								}
								else
								{
									type.InheritedMembers.Add(parentMember.Name, parentMember.ShallowCopy());
								}
							}
						}
					}

					this.FireProgressEvent(100);
				}
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error processing inherited members: {0}", ex.Message);
				throw;
			}
		}

		[OperationStep]
		internal void GenerateHtml()
		{
			var outputTypes = HtmlPageType.All;
			if (!this.Info.OutputDocuments)
			{
				outputTypes ^= HtmlPageType.Document;
			}

			this.GenerateHtml(outputTypes);
		}

		[OperationStep]
		internal void ReGenerateHtml()
		{
			var projectXml = new XmlDocument();
			projectXml.Load(this.ProjectFilePath);

			this.GenerateHtml(projectXml, HtmlPageType.All);
		}

		internal void GenerateHtml(HtmlPageType outputTypes)
		{
			var projectXml = new XmlDocument();
			projectXml.AppendChild(this.Item.ToXml(projectXml));

			this.GenerateHtml(projectXml, outputTypes);
		}

		internal void GenerateHtml(XmlDocument projectXml, HtmlPageType outputTypes)
		{
			try
			{
				var indexDoc = new XmlDocument();
				indexDoc.Load(this.ProjectIndexPath);

				var transformer = new TransformPackage(this);
				XmlElement projectNode = projectXml.DocumentElement;

				XmlNodeList documentNodes = projectXml.SelectNodes(@"/project/documents/document");

				XmlNodeList namespaceNodes = projectXml.SelectNodes(@"/project/members/namespaces/namespace");
				XmlNodeList memberNodes;

				if (!this.Info.OutputPrivateMembers)
				{
					XmlNodeList privates = projectXml.SelectNodes(@"//members/*/*[@visibility = 'private']");
					for (int i = 0; i < privates.Count; i++)
					{
						XmlNode p1 = privates[i].ParentNode;
						XmlNode p2 = privates[i].ParentNode.ParentNode;

						p1.RemoveChild(privates[i]);
						if (p1.ChildNodes.Count == 0)
						{
							p2.RemoveChild(p1);
						}
					}
				}

				if (this.Info.SinglePageMemberDetail)
				{
					memberNodes = projectXml.SelectNodes(@"/project/members/namespaces/namespace/members/*/*[not(@virtual)]");
				}
				else
				{
					memberNodes =
						projectXml.SelectNodes(@"//members/*/*[name() != 'namespace' and not(@virtual) and not(@inheritedFrom)]");
				}

				int stepCount = namespaceNodes.Count + memberNodes.Count + 4;
				int currentStep = 0;

				string filePath, fileName;
				if ((outputTypes & HtmlPageType.Project) == HtmlPageType.Project)
				{
					filePath = this.Info.GetProjectFileName(HtmlPageType.Project, true);
					fileName = Path.GetFileName(filePath);

					FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
					transformer.Transform(transformer.XsltProject, projectXml, projectNode, filePath);
					FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
				}

				var arguments = new Dictionary<string, object>
					{
						{ "groupBy", this.Info.GroupMembersBy.ToString().ToLower() },
						{ "singlePage", this.Info.SinglePageMemberDetail.ToString().ToLower() },
						{ "showPrivates", this.Info.ShowPrivateMembers ? "1" : "0" },
					};

				if ((outputTypes & HtmlPageType.Contents) == HtmlPageType.Contents)
				{
					filePath = this.Info.GetProjectFileName(HtmlPageType.Contents, true);
					fileName = Path.GetFileName(filePath);

					FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
					transformer.Transform(transformer.XsltContents, projectXml, projectXml, filePath, arguments);
					FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
				}

				if ((outputTypes & HtmlPageType.Index) == HtmlPageType.Index)
				{
					filePath = this.Info.GetProjectFileName(HtmlPageType.Index, true);
					fileName = Path.GetFileName(filePath);

					FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
					transformer.Transform(transformer.XsltIndex, indexDoc, indexDoc, filePath);
					FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
				}

				if ((outputTypes & HtmlPageType.Default) == HtmlPageType.Default)
				{
					filePath = this.Info.GetProjectFileName(HtmlPageType.Default, true);
					fileName = Path.GetFileName(filePath);

					FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
					transformer.Transform(transformer.XsltDefault, projectXml, projectXml, filePath);
					FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
				}

				if ((outputTypes & HtmlPageType.Namespace) == HtmlPageType.Namespace)
				{
					foreach (XmlElement ns in namespaceNodes)
					{
						filePath = this.Info.ExpandOutputPath(ns.GetAttribute("url"));
						fileName = Path.GetFileName(filePath);

						FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
						transformer.Transform(transformer.XsltNamespace, projectXml, ns, filePath);
						FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
					}
				}

				if ((outputTypes & HtmlPageType.Document) == HtmlPageType.Document)
				{
					foreach (XmlElement doc in documentNodes)
					{
						filePath = this.Info.ExpandOutputPath(doc.GetAttribute("url"));
						fileName = Path.GetFileName(filePath);

						FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
						transformer.Transform(transformer.XsltNamespace, projectXml, doc, filePath);
						FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
					}
				}

				if ((outputTypes & HtmlPageType.Object) == HtmlPageType.Object)
				{
					foreach (XmlElement obj in memberNodes)
					{
						filePath = this.Info.ExpandOutputPath(obj.GetAttribute("url"));
						fileName = Path.GetFileName(filePath);

						FireProgressEvent(fileName, 0, this.GetPercentDone(stepCount, currentStep));
						transformer.Transform(transformer.XsltObject, projectXml, obj, filePath, arguments);
						FireProgressEvent(fileName, 100, this.GetPercentDone(stepCount, ++currentStep));
					}
				}

				transformer.CopyResources();
			}
			catch (Exception ex)
			{
				log.ErrorFormat("Error generating html: {0}", ex.Message);
				throw;
			}
		}

		internal string ProcessString(string value, CodeItem member)
		{
			Func<Match, string> standardReplace = delegate(Match m)
				{
					string typeExpr = m.Groups["type"].Value;
					string textExpr = m.Groups["text"].Value;
					if (textExpr == string.Empty)
					{
						textExpr = typeExpr;
					}

					if (!string.IsNullOrEmpty(typeExpr))
					{
						string typeUrl = this.ResolveTypeUrl(typeExpr, member);
						if (typeUrl != null)
						{
							return this.Config.MakeLink(typeUrl, textExpr);
						}
					}

					return "<c>" + typeExpr + "</c>";
				};

			Func<Match, string> threePartReplace = delegate(Match m)
				{
					string prefix = m.Groups["prefix"].Value;
					string typeExpr = m.Groups["type"].Value;
					string suffix = m.Groups["suffix"].Value;

					if (!string.IsNullOrEmpty(typeExpr))
					{
						// matching function arguments are not code items and cannot be linked to.
						// we do want to emphasize them though.
						if (member is FunctionItem && ((FunctionItem)member).Arguments.ContainsKey(typeExpr))
						{
							return "<em>" + typeExpr + "</em>";
						}

						try
						{
							string typeUrl = this.ResolveTypeUrl(typeExpr, member);
							if (typeUrl != null)
							{
								return this.Config.MakeLink(typeUrl, typeExpr);
							}
						}
						catch
						{
							// An exception can happen when trying to resolve type url on expressions
							// that cannot (and should not) be resolved to types. We therefore ignore
							// any exceptions thrown by the previous block and let the coede pass through
							// to the next line.
						}
					}

					return string.Concat(prefix, typeExpr, suffix);
				};

			value = typeExpr1.Replace(value, new MatchEvaluator(standardReplace));
			value = typeExpr2.Replace(value, new MatchEvaluator(standardReplace));
			value = typeExpr3.Replace(value, new MatchEvaluator(standardReplace));
			value = typeExpr4.Replace(value, new MatchEvaluator(threePartReplace));

			return this.ProcessMarkup(value);
		}

		internal string ProcessMarkup(string value)
		{
			value = markupExpr1.Replace(
				value,
				delegate(Match m)
					{
						string content = m.Groups[1].Value;
						return string.Concat("<code>", content, "</code>");
					});
			return value;
		}

		internal CodeItem ResolveType(string typeName)
		{
			if (string.IsNullOrEmpty(typeName))
			{
				throw new ArgumentNullException("typeName");
			}

			return this.ResolveType(typeName, this.Item);
		}

		internal CodeItem ResolveType(string typeName, CodeItem member)
		{
			return this.ResolveType(typeName, member, 0);
		}

		/// <summary>
		/// Attempts to find the type specified with  <paramref name="typeName"/> and returns it's <see cref="CodeItem"/>
		/// if found.
		/// </summary>
		/// <param name="typeName">The expression that specifies the type to find.</param>
		/// <param name="member">The member in which the expression has been specified.</param>
		/// <param name="filter">Specifies a bitmask of <see cref="ItemType"/> values that the resulting type is allowed to resolve to.</param>
		/// <returns>
		/// The <see cref="CodeItem"/> associated with the supplied expression.
		/// </returns>
		/// <remarks>
		/// The search is first done within the supplied <paramref name="member"/>. If unsuccessful, the search is expanded
		/// to it's parent, until either a match is found, or there are no more parents to expand to.
		/// <para>
		/// Some examples of valid expressions:
		/// </para>
		/// 	<list type="bullet">
		/// 		<item><description>Object</description></item>
		/// 		<item><description>Object.toString</description></item>
		/// 		<item><description>Aeon.Controls.Slider</description></item>
		/// 		<item><description>Aeon.Controls.Slider.getValue</description></item>
		/// 	</list>
		/// </remarks>
		internal CodeItem ResolveType(string typeName, CodeItem member, ItemType filter)
		{
			if (string.IsNullOrEmpty(typeName))
			{
				throw new ArgumentNullException("typeName");
			}
			if (member == null)
			{
				throw new ArgumentNullException("member");
			}

			CodeItem fromMember = member;
			CodeItem resultMember = null;
			while (resultMember == null && fromMember != null)
			{
				resultMember = this.FindMember(typeName, fromMember);
				fromMember = fromMember.Parent;
			}

			if (resultMember == null)
			{
				resultMember = this.FindMember(typeName);
			}

			if (resultMember == null)
			{
				Log.WriteLine(LogCategory.Debug, "Could not resolve type {0}", typeName);
			}

			if (resultMember != null && filter != 0)
			{
				if (((int)(filter & resultMember.ItemType)) == 0)
				{
					resultMember = null;
				}
			}

			return resultMember;
		}

		internal string ResolveTypeUrl(string typeName, CodeItem currentMember)
		{
			CodeItem memberType = this.ResolveType(typeName, currentMember);
			if (memberType != null)
			{
				Log.WriteLine(LogCategory.Debug, "Resolved {0} to type {1}", typeName, memberType.Url);
				return memberType.Url;
			}

			var urls = this.Config.ExternalTypes.GetTypeUrl(typeName);
			if (urls.Count != 0)
			{
				//// TODO: Add support for multiple links
				string defaultUrl = urls[urls.Keys.FirstOrDefault()];
				Log.WriteLine(LogCategory.Debug, "Resolved {0} to external type {1}", typeName, defaultUrl);
				return defaultUrl;
			}

			Log.WriteLine(LogCategory.Debug, "Could not resolve {0} to a type", typeName);
			return null;
		}

		internal CodeItem FindMember(string typeName)
		{
			if (String.IsNullOrEmpty(typeName))
			{
				throw new ArgumentNullException("typeName");
			}

			CodeItem ns = this.Item.FindNamespace(typeName) ?? this.Item.FindNamespace(ParserConfig.GlobalNamespace);

			if (ns == null)
			{
				return null;
			}

			CodeItem result = this.FindMember(typeName, ns);
			return result;
		}

		internal CodeItem FindMember(string typeName, CodeItem searchIn)
		{
			if (String.IsNullOrEmpty(typeName))
				throw new ArgumentNullException("typeName");

			if (searchIn == null)
				throw new ArgumentNullException("searchIn");

			CodeItem result = searchIn.GetMember(typeName);
			if (result != null)
				return result;

			result = searchIn;
			string memberName = Regex.Replace(typeName, result.ExpandedName + @"\.(.*)(?:(\..*)$)?", "$1");

			string[] parts = memberName.Split(new[] { '.' });
			foreach (string t in parts)
			{
				result = result.GetMember(t);
				if (result == null)
					break;
			}

			return result;
		}

		private void ProcessOperation(ProjectOperation operation)
		{
			if (this.Processing)
			{
				return;
			}

			try
			{
				this.Processing = true;
				this.progressArgs = new ProjectOperationProgressEventArgs(this.Info.Name, operation);
				var stepNames = operation.Steps.Keys.ToList();
				for (int i = 0; i < stepNames.Count; i++)
				{
					string stepName = stepNames[i];
					FireProgressEvent(i, 0, 0);
					Log.WriteLine(LogCategory.Info, "Processing step {0}: {1}.", i + 1, stepName);
					operation.Steps[stepName].Invoke();
					FireProgressEvent(i, 100, 100);
				}

				this.FireProgressEvent(true);
			}
			finally
			{
				this.Processing = false;
			}
		}

		private void Reset()
		{
			this.Files = new List<string>();
			this.Item = new ProjectItem(this.Info);
			this.progressArgs = null;
		}

		private void Save()
		{
			var resultDoc = new XmlDocument();
			resultDoc.AppendChild(this.Item.ToXml(resultDoc));
			resultDoc.Save(this.ProjectFilePath);

			XmlDocument indexDoc = this.Item.GenerateIndex(this.Info.OutputPrivateMembers);
			indexDoc.Save(this.ProjectIndexPath);
		}

		private IEnumerable<string> GetFiles(PathInfo pathInfo)
		{
			if (pathInfo == null)
			{
				throw new ArgumentNullException("pathInfo");
			}

			string path = this.Info.ResolvePath(pathInfo.Path);
			string extensions = pathInfo.IncludeExtensions;

			string[] list = Directory.GetFiles(path, extensions, SearchOption.AllDirectories);
			var result = new List<string>(list.Where(pathInfo.IsValidFile));

			return result;
		}

		private DocumentItem ParseFile(ProjectItem project, Parser parser, string filePath)
		{
			this.PrepareOutput();

			DocumentItem result = parser.ParseFile(project, filePath);
			var resultDoc = new XmlDocument();
			resultDoc.AppendChild(result.ToXml(resultDoc));

			string resultPath = Path.Combine(
				this.Info.ArtifactPath, String.Concat(Path.GetFileNameWithoutExtension(filePath), ".xml"));
			resultDoc.Save(resultPath);

			return result;
		}

		private void PrepareOutput()
		{
			if (!Directory.Exists(this.Info.OutputPathFull))
			{
				Directory.CreateDirectory(this.Info.OutputPathFull);
			}

			if (!Directory.Exists(this.Info.ArtifactPath))
			{
				Directory.CreateDirectory(this.Info.ArtifactPath);
			}
		}

		private int GetPercentDone(int total, int index)
		{
			float percent = total / 100F;
			index = index > total ? total : index;

			return Convert.ToInt32(Math.Round(index / percent));
		}

		private void FireProgressEvent(int fileDone)
		{
			this.progressArgs.PercentDoneCurrentFile = fileDone;

			this.FireProgressEvent(false);
		}

		private void FireProgressEvent(string objectName, int fileDone, int stepDone)
		{
			this.progressArgs.ObjectName = objectName;
			this.progressArgs.PercentDoneCurrentFile = fileDone;
			this.progressArgs.PercentDoneOperationStep = stepDone;

			this.FireProgressEvent(false);
		}

		private void FireProgressEvent(int stepIndex, int fileDone, int stepDone)
		{
			this.progressArgs.StepIndex = stepIndex;
			this.progressArgs.PercentDoneCurrentFile = fileDone;
			this.progressArgs.PercentDoneOperationStep = stepDone;

			this.FireProgressEvent(false);
		}

		private void FireProgressEvent(bool complete)
		{
			if (complete)
			{
				this.progressArgs.Complete = true;
			}

			if (this.ProcessProgress != null)
			{
				this.ProcessProgress(this, this.progressArgs);
			}
		}
	}
}